#region License, Terms and Author(s)
//
// TempRESS - TEMPlate Rendering Engine that's Small and Simple 
// Copyright (c) 2009 Atif Aziz. All rights reserved.
//
//  Author(s):
//
//      Atif Aziz, http://www.raboof.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

namespace Tempress
{
    #region Imports

    using System;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using System.Text.RegularExpressions;

    #endregion

    public sealed class Template
    {
        public static TemplateCodeContext Parse(TemplateParseContext context, TextReader reader, TextWriter writer)
        {
            if (context == null) throw new ArgumentNullException("context");
            if (reader == null) throw new ArgumentNullException("reader");
            
            var result = new TemplateCodeContext();
            var imports = new List<string>();

            using (var token = Parser.Parse(reader).GetEnumerator())
            {
                while (token.MoveNext())
                {
                    var directive = token.Current as Parser.DirectiveToken;
                    if (directive == null)
                        break;

                    switch (directive.Directive.ToLowerInvariant())
                    {
                        case "template":
                        {
                            var name = directive.Attributes["name"];
                            var lastDotIndex = name.LastIndexOf('.') + 1;
                            result.ClassName = name.Substring(lastDotIndex);
                            result.Namespace = name.Substring(0, lastDotIndex).TrimEnd('.');
                            break;
                        }
                        case "assembly":
                        {
                            result.Assemblies.Add(directive.Attributes["name"]);
                            break;
                        }
                        case "import":
                        {
                            imports.Add(directive.Attributes["namespace"]);
                            break;
                        }
                        case "argument":
                        {
                            var name = directive.Attributes["name"];
                            var type = directive.Attributes["type"];
                            result.Arguments.Add(new TemplateArgument(name, type));
                            break;
                        }
                        default:
                            throw new TemplateCompilationException(string.Format("Unknown '{0}' directive.", directive.Directive));
                    }
                }

                var templateScript = new StringBuilder();
                var renderCode = new StringBuilder();

                do
                {
                    if (token.Current is Parser.ScriptToken)
                    {
                        templateScript.AppendLine(((Parser.ScriptToken) token.Current).Script);
                    }
                    else if (token.Current is Parser.LiteralToken)
                    {
                        renderCode.Append("Response.Write(@\"")
                                  .Append(((Parser.LiteralToken) token.Current).Text.Replace("\"", "\"\""))
                                  .AppendLine("\");");
                    }
                    else if (token.Current is Parser.CodeToken)
                    {
                        renderCode.AppendLine(((Parser.CodeToken) token.Current).Code);
                    }
                    else if (token.Current is Parser.ExpressionToken)
                    {
                        renderCode.Append("Response.Write(")
                                  .Append(((Parser.ExpressionToken) token.Current).Expression)
                                  .AppendLine(");");
                    }
                }
                while (token.MoveNext());

                writer.WriteLine (@"using System;");
                foreach (var import in imports)
                    writer.WriteLine("using {0};", import);

                if (result.ClassName.Length == 0)
                    result.ClassName = "TempGeneratorClass";

                if (result.Namespace.Length > 0)
                    writer.WriteLine(@"namespace {0} {{", result.Namespace);
                
                writer.WriteLine("partial class {0} {{", result.ClassName);

                writer.Write(templateScript);

                foreach (var argument in result.Arguments)
                {
                    writer.WriteLine(
                        @"    private {1} __{0}; 
    public {1} {0} {{ get {{ return __{0}; }} set {{ __{0} = value; }} }}", 
                        argument.Name, argument.ArgumentType);
                }

                writer.Write(@"
    System.IO.TextWriter Response;
    public void Generate(object[] args, System.IO.TextWriter writer) {
    this.Response = writer;
    try {
    ");
                var argIndex = 0;
                foreach (var arg in result.Arguments)
                    writer.WriteLine("this.{0} = ({1}) args[{2}];", arg.Name, arg.ArgumentType, argIndex++);

                writer.Write(@"__render__();
    } catch { Response.Flush(); throw; }
    } 
    private void __render__() {
    ");
                writer.WriteLine(renderCode);
                writer.Write("} }");

                if (result.Namespace.Length > 0)
                    writer.Write(" }");
                writer.WriteLine();

                writer.Flush();
            }

            return result;
        }

        internal static class Parser
        {
            private sealed class TextPosition
            {
                public TextPosition(string text) : 
                    this(text, 0, 1, 1) {}

                private TextPosition(string text, int index, int line, int column)
                {
                    Text = text;
                    Index = index;
                    Line = line;
                    Column = column;
                }

                public string Text { get; private set; }
                public int Index { get; private set; }
                public int Line { get; private set; }
                public int Column { get; private set; }
                // ReSharper disable InconsistentNaming
                public bool EOF { get { return Index >= Text.Length; } }
                // ReSharper restore InconsistentNaming

                public void Scroll(int newIndex)
                {
                    var index = Index;
                    var line = Line;
                    var column = Column;
                    while (index < newIndex)
                    {
                        var ch = Text[index++];
                        if (ch == '\n')
                        {
                            line++;
                            column = 1;
                        }
                        else if (ch != '\r')
                        {
                            column++;
                        }
                    }
                    Line = line;
                    Column = column;
                    Index = index;
                }

                public void SkipWhiteSpace()
                {
                    var index = Index;
                    var text = Text;
                    while (index < text.Length && char.IsWhiteSpace(text, index))
                        index++;
                    Scroll(index);
                }

                public override string ToString()
                {
                    return new StringBuilder()
                        .Append("Position = ").Append((Index + 1).ToString("N0"))
                        .Append(", Line = ").Append(Line.ToString("N0"))
                        .Append(", Column = ").Append(Column.ToString("N0"))
                        .AppendLine(":")
                        .Append(Text.Substring(Index))
                        .ToString();
                }
            }

            public abstract class Token { }
            
            [ DebuggerDisplay("Text = {Text}") ]
            // ReSharper disable MemberCanBePrivate.Local
            public sealed class CommentToken : Token
            // ReSharper restore MemberCanBePrivate.Local
            {
                // ReSharper disable MemberCanBePrivate.Local
                public string Text { get; private set; }
                // ReSharper restore MemberCanBePrivate.Local
                public CommentToken(string text) { Text = text ?? string.Empty; }
                public override string ToString() { return "<%-- " + Text + " --%>"; }
            }

            [ DebuggerDisplay("Text = {Text}") ]
            public sealed class LiteralToken : Token
            {
                public string Text { get; private set; }
                public LiteralToken(string text) { Text = text ?? string.Empty; }
                public override string ToString() { return Text; }
            }

            [ DebuggerDisplay("Expression = {Expression}") ]
            public sealed class ExpressionToken : Token
            {
                public string Expression { get; private set; }

                public ExpressionToken(string expression)
                {
                    Debug.Assert(expression != null);
                    Debug.Assert(expression.Length > 0);
                    Expression = expression;
                }

                public override string ToString() 
                {
                    return "<%= " + Expression + " %>";
                }
            }

            [ DebuggerDisplay("Code = {Code}") ]
            public sealed class CodeToken : Token
            {
                public string Code { get; private set; }

                public CodeToken(string code)
                {
                    Debug.Assert(code != null);
                    Debug.Assert(code.Length > 0);
                    Code = code;
                }

                public override string ToString()
                {
                    return "<% " + Code + " %>";
                }
            }

            [ DebuggerDisplay("Script = {Script}") ]
            public sealed class ScriptToken : Token
            {
                public string Script { get; private set; }

                public ScriptToken(string script)
                {
                    Debug.Assert(script != null);
                    Debug.Assert(script.Length > 0);
                    Script = script;
                }

                public override string ToString()
                {
                    return "<script runat=\"template\">" 
                         + Environment.NewLine 
                         + Script 
                         + Environment.NewLine 
                         + "</script>";
                }
            }

            [ DebuggerDisplay("Directive = {Directive}") ]
            public sealed class DirectiveToken : Token
            {
                public string Directive { get; private set; }
                public IDictionary<string, string> Attributes { get; private set; }

                public DirectiveToken(string name, IDictionary<string, string> attributes)
                {
                    Directive = name;
                    Attributes = attributes;
                }

                // ReSharper disable UnusedMember.Local
                public string GetAttribute(string name, string defaultValue)
                // ReSharper restore UnusedMember.Local
                {
                    string value;
                    return Attributes.TryGetValue(name, out value) ? value : (defaultValue ?? string.Empty);
                }

                public override string ToString()
                {
                    var sb = new StringBuilder().Append("<%@").Append(Directive);
                    foreach (var attribute in Attributes)
                        sb.Append(' ').Append(attribute.Key).Append("=\"").Append(attribute.Value).Append('\"');
                    return sb.Append(" %>").ToString();
                }
            }

            private static class Gex // Grammar expressions
            {
                public static readonly Regex Expression;
                public static readonly Regex Code;
                public static readonly Regex Directive;
                public static readonly Regex Tag;
                public static readonly Regex Comment;

                static Gex()
                {
                    Directive = Regex(@"\G 
                          <% \s* @
                             \s* (?<d>\w[\w:]*(?=\W))
                            (\s* (?<a>\w[\w:]*(?=\W)) (
                                      \s*=\s*""(?<v>[^""]*)""
                                    | \s*=\s*' (?<v>[^'] *)'))*
                          \s*? %>");
                    Tag = Regex(@"\G 
                          < (?<t>[\w:\.]+)
                            (\s* (?<a>\w[-\w:]*) (
                                      \s*=\s*""(?<v>[^""]*)""
                                    | \s*=\s*' (?<v>[^'] *)'))*
                          \s*? >");
                    Expression = Regex(@"\G <% \s*? = (?<code>.*?)? %>");
                    Code = Regex(@"\G <% (?!@) (?<code>.*?) %>");
                    Comment = Regex(@"\G<%--(([^-]*)-)*?-%>");
                }

                static Regex Regex(string pattern)
                {
                    return new Regex(pattern,
                        RegexOptions.Singleline 
                        | RegexOptions.Multiline 
                        | RegexOptions.IgnorePatternWhitespace 
                        | RegexOptions.CultureInvariant);
                }
            }

            public static IEnumerable<Token> Parse(TextReader reader)
            {
                var source = reader.ReadToEnd();

                var pos = new TextPosition(source);
                while (true)
                {
                    pos.SkipWhiteSpace();
                    var match = Gex.Directive.Match(source, pos.Index);
                    if (!match.Success)
                        break;
                    pos.Scroll(match.Index); 
                    var groups = match.Groups;
                    var names = groups["a"].Captures;
                    var values = groups["v"].Captures;
                    yield return new DirectiveToken(groups["d"].Value, MapAttributes(names, values));
                    pos.Scroll(match.Index + match.Length);
                }

                var literal = new StringBuilder();

                while (true)
                {
                    var index = pos.Index;
                    pos.SkipWhiteSpace();
                    if (pos.Index > index) 
                        literal.Append(pos.Text, index, pos.Index - index);
                    
                    if (pos.EOF)
                        break;

                    Token token = null;
                    int? tokenEndIndex = null;

                    var match = Gex.Comment.Match(source, pos.Index);
                    if (match.Success)
                    {
                        token = new CommentToken(match.Value.Substring(2, match.Value.Length - 4).Trim('-'));
                    }
                    else
                    {
                        match = Gex.Expression.Match(source, pos.Index);
                        if (match.Success)
                        {
                            token = new ExpressionToken(match.Groups["code"].Value);
                        }
                        else
                        {
                            match = Gex.Code.Match(source, pos.Index);
                            if (match.Success)
                            {
                                token = new CodeToken(match.Groups["code"].Value);
                            }
                            else
                            {
                                match = Gex.Tag.Match(source, pos.Index);
                                {
                                    if (match.Success)
                                    {
                                        var groups = match.Groups;
                                        if (groups["t"].Value.Equals("script", StringComparison.OrdinalIgnoreCase))
                                        {
                                            var attributes = MapAttributes(groups["a"].Captures, groups["v"].Captures);
                                            string runat;
                                            if (attributes.TryGetValue("runat", out runat)
                                                && "template".Equals(runat, StringComparison.OrdinalIgnoreCase))
                                            {
                                                var start = match.Index + match.Length;
                                                const string scriptEndTag = "</script>";
                                                var end = source.IndexOf(scriptEndTag, start, StringComparison.OrdinalIgnoreCase);
                                                if (end < 0)
                                                    throw new Exception(string.Format("Unterminated script starting at line {0:N0}, column {1:N0}.", pos.Line, pos.Column));
                                                token = new ScriptToken(source.Substring(start, end - start));
                                                tokenEndIndex = end + scriptEndTag.Length;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

                    if (token != null)
                    {
                        if (literal.Length > 0)
                        {
                            yield return new LiteralToken(literal.ToString());
                            literal.Length = 0;
                        }
                        yield return token;
                        pos.Scroll(tokenEndIndex ?? match.Index + match.Length);
                    }
                    else
                    {
                        index = pos.Text.IndexOf('<', pos.Index + 1);
                        if (index > 0)
                        {
                            literal.Append(pos.Text, pos.Index, index - pos.Index);
                            pos.Scroll(index);
                        }
                        else
                        {
                            break;
                        }
                    }
                }

                if (!pos.EOF)
                {
                    literal.Append(pos.Text, pos.Index, pos.Text.Length - pos.Index);
                    yield return new LiteralToken(literal.ToString());
                }
            }

            private static Dictionary<string, string> MapAttributes(CaptureCollection names, CaptureCollection values) 
            {
                var attributes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                for (var i = 0; i < names.Count; i++)
                    attributes.Add(names[i].Value, values[i].Value);
                return attributes;
            }
        }
        
        public static CompiledTemplate Compile(TemplateCompilationContext context, TemplateCodeContext sourceContext, TextReader reader)
        {
            if (sourceContext == null) throw new ArgumentNullException("sourceContext");
            if (reader == null) throw new ArgumentNullException("reader");

            var codeProvider = CodeDomProvider.CreateProvider("c#");

            var parameters = context.CompilerParameters ?? new CompilerParameters();
            parameters.GenerateExecutable = false;
            
            var references = parameters.ReferencedAssemblies;
            references.Add("System.dll");
            foreach (var reference in sourceContext.Assemblies)
                references.Add(context.ResolveAssemblyPath(reference));

            var source = reader.ReadToEnd();
            var results = codeProvider.CompileAssemblyFromSource(parameters, source);
            
            if (results.Errors.Count > 0)
            {
                var errors = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    errors.Append("Template: ").AppendLine(error.FileName)
                          .Append("Line number: ").AppendLine(error.Line.ToString())
                          .Append("Error: ").Append(error.ErrorNumber).Append(" '").Append(error.ErrorText).Append("'");
                    var line = GetLine(source, error.Line);
                    
                    throw new TemplateCompilationException(
                        "Error compiling template: " 
                        + Environment.NewLine 
                        + errors 
                        + Environment.NewLine 
                        + "Line: '" + line + "'");
                }
            }

            var assembly = results.CompiledAssembly;
            var type = assembly.GetType(sourceContext.Namespace + "." + sourceContext.ClassName);
            Debug.Assert(type != null);
            var generator = Activator.CreateInstance(type);
            var processor = (CompiledTemplate.ProcessHandler) Delegate.CreateDelegate(typeof(CompiledTemplate.ProcessHandler), generator, "Generate");
            return new CompiledTemplate(processor, sourceContext.Arguments);
        }

        private static string GetLine(string source, int lineNo)
        {
            var reader = new StringReader(source);
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                if (--lineNo == 0)
                    return line;
            }
            return string.Empty;
        }
    }
}

#region Historical Note and Credit
//
// TempRESS was originally forked from TemplateMaschine after several failed 
// attempts at contacting its author. It has since been re-factored and 
// nearly re-written. Below is a reproduction of the header to original 
// source.
//
/* ---------------------------------------------------------------
    TemplateMaschine - an open source template engine for C#

    written by Stefan Sarstedt (http://www.stefansarstedt.com/)
    Released under GNU Lesser General Public License (LGPL),
    see file 'copying' for details about the license

    History:
        - initial release (version 0.5) on Oct 28th, 2004
        - minor bugfixes (version 0.6) on March 29th, 2005
        - updated to support referencing assemblies that are 
          installed in the GAC (version 0.7) on January 12th, 2007
          Thanks to William.Manning@ips-sendero.com
        - Added support for generic arguments. 
          Ability to pass dictionary of arguments relaxing ordered 
          object[] requirement. Version 0.8 on March 18th, 2007
          Thanks to vijay.santhanam@gmail.com
   --------------------------------------------------------------- */
#endregion
